跳到主要内容

游戏开发中的 MVC 模式

注意:这里只是谈谈我理解的 MVC 模式(游戏),而实际后端的 MVC 模式和这个不一样

MVC 的构成

MVC 是三个单词的缩写,分别为:模型(Model)、视图(View)和控制(Controller)。

模型是数据层,视图是表现层,控制器是逻辑层,也对应于程序运行中的数据输入,数据处理,数据输出基本三步骤。

注意:MVC 这个模式是由三个基本的设计模式组成的

  • 策略模式
  • 观察者模式
  • 组合模式

如上图所示,策略模式表示 MVC 中的 Controller 部分。策略模式将用户输入与游戏的逻辑(Model)和接口(View)分离。

组合设计模式表示应用程序中的所有视图(主窗口和按钮)。此设计模式为模型的所有视图提供统一的访问点。

观察者模式代表你的应用程序(模型)中的逻辑。通过这种模式,模型能够与视图和控制器进行交互,而不需要知道它们内部细节。这种模式使所有类之间的交互有更低的耦合度。

注意:一般所有通信都是单向的。

View         传送指令到 Controller
Controller 完成业务逻辑后,要求 Model 改变状态
Model 将新的数据发送到 View,用户得到反馈

互动模式的两种方式

接受用户指令时,MVC 可以分成两种方式。一种是通过 View 接受指令,传递给 Controller。

另一种是直接通过 Controller 接受指令。

数据层

数据层就是各种资源(图片,声音,动画)在游戏引擎中形成的对象集合。

美术提供的这些是最原始的,需要游戏引擎封装成一组可控的代码。

在 Cocos2D 中就用相应的对象来直接初始化这些原始资源,比如 Sprit,Menu,SimpleAudioEngine 等等。这些对象在整个游戏中可以划分为数据层。

最好的例子就是 CocosBuilder 生成的 ccb 文件通过引擎解析而成的类。当最原始的资源文件通过一定的文件格式组织起来,进而通过引擎形成对象集合,这是最可取的数据层生成方法。

比如多国语言的本地化。所有的字段的翻译通过一个 plist 文件存储,形成一张二维表。当需要切换语言时,只需要改变某个 key 相对的 Value 就行了。逻辑层不需要做任何改变。

数据层中的观察者模式

观察者模式是实现 MVC 的关键模式。或者说,MVC 模式就是基于观察者模式的。

在 MVC 模式中有3个参与者,其各自职责如下:

视图(View):负责显示数据(只负责显示数据,不做其他操作);负责通知用户在视图界面上的操作(给控制器或其他参与者)。

控制器(Controller):负责事件处理;负责显示逻辑/业务逻辑。

模型(Model or ViewModel):

  • 负责获取数据(视图的);
  • 负责维持数据状态;
  • 负责通知数据变更(给视图或控制器)。

可以看到,在MVC各参与者都有自己,他们之间用消息机制传递变化。

监听函数的悬垂引用

监听者模式实现了各参与者的解耦,通过消息来实现相互协作。发布者不知道订阅者的具体功能与运行状态。订阅者知道发布者存在,以及会在某个不特定的时刻调用它注册的监听函数;除此之外,它对订阅者也一无所知。订阅者的实体对象与发布者实体对象的生命周期也完全无关,可能订阅者实体对象先被销毁,也可能发布者先于订阅者被销毁。

通常订阅者会在内部维护一个订阅者的监听者列表。当订阅者先于发布者被销毁,订阅者注册到发布者中去的监听函数是无效函数。

可以让订阅者采用 Dispose 模式来避免监听函数的悬垂引用:

订阅者提供一个 dispose 函数,订阅者显示调用 dispose 来结束生命周期。在 dispose 函数中,订阅者退订消息。 采取某种垃圾回收机制,当检测到订阅者不再有效时,垃圾回收代码调用订阅者的 dispose 函数来退订消息。

例如:

public void OnDestroy() {
EventManager.Remove(this);
}

逻辑层:策略模式

逻辑层可以划分为:

  • 数据接口层
  • 游戏 AI 层
  • 操作控制层。

数据接口层 是负责从数据层中提供相应的数据对象进行封装组合。在这个层次上,类似与面向对象中类的属性定义,并提供控制(Set/Get)接口。这一层既可以是无结构化(原始数据类型定义),也可以是结构化(表,树,集合)。

游戏 AI 层 是逻辑层之核心,定义了其下面的各种数据元素的状态变化(金币数量,武器等级,动画播放……)。根据不同的状态变量,控制着数据元素的状态表现,是游戏的大脑,指挥控制中心。

操作控制层,负责处理用户的输入,并注册或绑定相应的 AI 事件。

这里拿操作控制层举例:

在游戏中,你应该将输入控制器和游戏逻辑之间的交互进行解耦。游戏的逻辑应该接收相同类型的输入,而不管输入控制器是什么(按钮,手势,操纵杆)。

尽管对用户每个输入控制器的行为表现不同,但它们必须向游戏的逻辑提供相同的数据。此外,添加或删除输入控制器不应导致游戏崩溃。

这种解耦行为和灵活性是可能的,这归功于策略设计模式。这种设计模式允许通过动态方式来改变行为,而不需要修改游戏的任何逻辑,为你的游戏提供了很高的灵活性。

总之:在逻辑层中,数据接口层向下面对数据层,操作控制层向上面对用户行为事件,而游戏AI层统筹这两层。

视图层:组合模式

游戏通常包含许多视图。主视图中显示角色。有一个子视图,显示玩家的积分。有一个子视图,显示游戏中剩下的时间。如果你在移动设备上玩游戏,那么每个按钮都是一个视图。

可维护性应该是游戏开发过程中的主要关注点。每个视图不应具有不同的函数名称或不同的访问点。相反,你想要为每个视图提供一个统一的访问点,即相同的函数调用应该既能够访问主视图也能够访问子视图。

这种统一的接入点可以使用复合设计模式。此模式将每个视图放置在树状结构中,从而为每个视图提供统一的访问点。取代了需要用不同的函数来访问不同的子视图,组合模式可以用相同的函数访问任何视图。

视图层的职责

View 负责与用户的交互,交互又分为两种:输入和输出。

输入:输入就是收集玩家的操作,例如玩家点击了一个按钮,或者输入了某些文字。例如,当玩家点击了一个好友的头像,或者点击了删除好友。就需要对这些输入进行响应,对数据做出相应的处理。

输出:输出就是将游戏中各种数据,展示出来(包括视图展示,特效,声音等),让玩家能够看清楚,看明白。例如,上述中的 FriendModel 中保存的数据,输出就负责将其显示出来,让玩家看到这个好友的性别是男或者女,而不是1或者0,或是其它什么的。

补充:MVP 模式

MVP 模式将 Controller 改名为 Presenter,同时改变了通信方向。

  1. 各部分之间的通信,都是双向的。
  2. View 与 Model 不发生联系,都通过 Presenter 传递。
  3. View 非常薄,不部署任何业务逻辑,称为 "被动视图"(Passive View),即没有任何主动性,而 Presenter 非常厚,所有逻辑都部署在那里。

Presenter 负责和 Model 进行双向交互,还有和 View 进行双向交互所以如果业务复杂一点,Presenter的体积增大、臃肿,就很难维护;

补充:MVVM 模式

MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。即:Model-View-ViewModel

唯一的区别是,它采用双向绑定(data-binding):View 的变动,自动反映在 ViewModel,反之亦然。

Angular 和 Ember 都采用这种模式。

ViewModel,主要是胶水层,核心思想是简化,它做两件事:

  1. 数据发生变化,如何知道变化,通过数据响应式的机制,用某种机制知道这个数据的变化,自动的去响应数据的变化,自动去做更新;内部知道了数据的变化,不需要用户来操作。
  2. 更新:以前是直接用 JQ 进行 Dom 操作,每次数据变了都要自己操作 Dom 来更新(代码多,效率不高);所以有了虚拟 Dom 的方式去做更新,根据精准的 diff 算法来做比对;达到高效的结果。

补充:前后端 MVC 区别

后端 MVC 中的 view 是前端 MVC 的全部,一般来说,前端MVC大部分都是MVVM(MVC的强类型转换高级版),包括 angularJS(当然后端中也有 MVVM 的,比如 WPF)。

Reference

谈谈UI架构设计的演化 观察者模式与MVC模式 MVC,MVP 和 MVVM 的图示